package cn.akwangl.interceptor;

import cn.akwangl.exception.BizException;
import cn.akwangl.pojo.User;
import cn.hutool.core.util.StrUtil;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTHeader;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;
import org.slf4j.Logger;

import java.security.AccessControlContext;

/**
 * 全局用户实体.
 *
 * @author 阿坤同学
 * @date 2022/5/29 12:56
 */
public class GlobalUser {

    private Logger log = LoggerWrapper.getLogger(GlobalUser.class);

    /**
     * 使用{@link InheritableThreadLocal}来解决异步子线程与主线程数据共享的问题及注意事项.
     * <p>
     * 关于{@link InheritableThreadLocal}类为什么可以实现子线程共享父线程数据.
     * 其实这个问题直接看{@link Thread#init(ThreadGroup, Runnable, String, long, AccessControlContext, boolean)},
     * 这个初始化方法细节就可以完全弄清楚,到底是在什么时候将数据放到子线程中,下面我就罗列出一下关键代码.
     * <pre>
     *      private void init(ThreadGroup g, Runnable target, String name,
     *                       long stackSize, AccessControlContext acc,
     *                       boolean inheritThreadLocals) {
     *
     *          // 在创建时,获取当前线程(相对于子线程的父线程)
     *          Thread parent = currentThread();
     *          // 将父线程中的parent.inheritableThreadLocals 拷贝到子线程中去,至此子线程就可以共享父线程中的数据.
     *          if (inheritThreadLocals && parent.inheritableThreadLocals != null)
     *            this.inheritableThreadLocals =
     *               ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
     *      }
     * </pre>
     * 总结一下:{@link InheritableThreadLocal}只会在{@link Thread}初始化时,才会将数据放到新的线程中去,假如你的业务有深拷贝问题
     * 那么需要自己重写{@link InheritableThreadLocal#childValue(Object)}方法.<br/>
     * 现在了解了{@link InheritableThreadLocal}数据会再线程创建时将数据拷贝到子线程中去,那要是有一个线程池刚好他没有超过最大核心数,那岂不是不会拷贝数据?<br/>
     * 是的会存在这样的问题,所以{@link TransmittableThreadLocal}就孕育而出了.<br/>
     * <p>
     * {@link TransmittableThreadLocal}简称(TTL),TTL使用装饰者模式对原有{@link InheritableThreadLocal}在线程池中线程复用传递数据问题进行了增强.
     * <p>
     * 下面讲解一下TTL拷贝数据到子线程中的一些细节.<br/>
     * 1. {@link TransmittableThreadLocal#holder}保存当前线程下的所有TTL线程变量,在每次调用{@link TransmittableThreadLocal#set(Object)}和{@link TransmittableThreadLocal#get()}
     * 方法时都会将数据放到{@link TransmittableThreadLocal#holder}中,为什么要存一份?是为了后面在提交任务时将父线程中的数据拷贝到子线程中,
     * 那为什么要设计为Map类型?因为一个{@link Thread}类可以包含多个{@link ThreadLocal},为了能确保子线程数据与父线程一致所以在备份时设计为Map类型.<br/>
     * 2. 什么时候进行会将父线程数据拷贝到子线程中?<br/>
     * 在构建{@link TtlRunnable}就会调用{@link TransmittableThreadLocal.Transmitter#capture()}方法将父线程的{@link ThreadLocal}数据备份到{@link TransmittableThreadLocal.Transmitter.Snapshot}中<br/>
     * 在{@link TtlRunnable#run()}被执行是会将备份的数据设值到子线程的{@link ThreadLocal}中以达到复用的效果,当{@link TtlRunnable#runnable},
     * 运行完后则会将备份数据再次恢复到提交任务之前的状态,避免在任务运行过程中修改{@link Thread}中其他{@link ThreadLocal}的数据对其他引用发生数据错误.<br/>
     *
     * <p>
     * 文献:<br/>
     * <a href="https://zhuanlan.zhihu.com/p/146124826">全链路追踪必备组件之 TransmittableThreadLocal 详解</a><br/>
     * <a href="https://blog.csdn.net/wk52525/article/details/107859685">TransmittableThreadLocal（TTL）实现线程变量传递的原理分析</a>
     *
     * @since 0.0.4
     */
    private static TransmittableThreadLocal<User> globalUser = new TransmittableThreadLocal();

    /**
     * 设置全局User实体.
     *
     * @param token jwt令牌
     * @author 阿坤同学
     * @date 2022/5/29 13:02
     * @since 0.0.4
     */
    void setGlobalUser(String token) {
        if (StrUtil.isBlank(token)) {
            throw new BizException("认证失败,token不存在");
        }

        JWT jwt = JWT.of(token);
        // HS256
        jwt.getHeader(JWTHeader.ALGORITHM);
        long expire = (Long) jwt.getPayload("expire");

        boolean hasExpire = System.currentTimeMillis() > expire;
        if (hasExpire) {
            throw new BizException("认证失败,token已过期");
        }

        User user = new User();
        user.setId(Long.parseLong(jwt.getPayload("id").toString()));
        user.setName((String) jwt.getPayload("name"));
        globalUser.set(user);
        log.info("globalUser 赋值成功");
    }

    /**
     * 清除全局User实体.
     *
     * @author 阿坤同学
     * @date 2022/5/29 13:02
     * @since 0.0.4
     */
    void cleanGlobalUser() {
        globalUser.remove();
        log.info("globalUser 清除成功");
    }

    /**
     * 用户实体.
     *
     * @return User
     * @author 阿坤同学
     * @date 2022/5/29 13:06
     * @since 0.0.4
     */
    User getGlobalUser() {
        return globalUser.get();
    }
}